TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import Link from "next/link";2import type { Metadata } from "next";3import { notFound } from "next/navigation";4import { EventRow } from "@/components/event-row";5import { FieldChanges } from "@/components/field-changes";6import { LiveFeed } from "@/components/live-feed";7import { Badge, Bar, Chip, Empty, ExtLink, Flag, Heatmap, PageHeader, Panel, Score, Stat, Table, Tabs, Td, TierBadge, TypeChip } from "@/components/ui";8import { WatchButton } from "@/components/watch-button";9import { api, SITE_URL, type EntityDetail, type EntityInsights, type EventItem } from "@/lib/api";10import { agoIso, dayHeader, fmtInt, fmtScore, relTime, typeLabel, utcDate, utcDateTime, utcTime, withinLast } from "@/lib/format";1112export const dynamic = "force-dynamic";1314type Tab = "overview" | "live" | "sources" | "silent" | "timeline" | "related" | "metrics";15const TABS: Tab[] = ["overview", "live", "sources", "silent", "timeline", "related", "metrics"];16const RANGES: { key: string; label: string; ms: number | null }[] = [17 { key: "1h", label: "1 h", ms: 3600e3 },18 { key: "6h", label: "6 h", ms: 6 * 3600e3 },19 { key: "24h", label: "24 h", ms: 24 * 3600e3 },20 { key: "7d", label: "7 d", ms: 7 * 86400e3 },21 { key: "30d", label: "30 d", ms: 30 * 86400e3 },22 { key: "all", label: "All", ms: null },23];2425type Search = { tab?: string; range?: string; cursor?: string };2627export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {28 const { id } = await params;29 const d = await api.entity(id);30 if (!d) return { title: "Entity not found" };31 const e = d.entity;32 const desc = e.description ?? `Every meaningful change WebSensor detected for ${e.name}: announcements, releases, pricing, documentation, incidents and silent changes — with evidence.`;33 return { title: `${e.name} — ${typeLabel(e.type)}`, description: desc, alternates: { canonical: `/entity/${e.id}` }, openGraph: { title: `${e.name} · WebSensor`, description: desc, url: `${SITE_URL}/entity/${e.id}` } };34}3536const EMPTY_INSIGHTS: EntityInsights = { heatmap: [], baseline_per_day: 0, today: 0, events_24h: 0, events_prev_24h: 0, velocity_ratio: 0, anomaly: { score: 0, ratio: 0, pct: 0 }, silent_24h: 0, breaking_24h: 0, sources_24h: 0, most_active_sensors: [], rank: { rank: null, total: 0, score: null } };3738function mainCountry(d: EntityDetail): string | null {39 const counts = new Map<string, number>();40 for (const s of d.sources) if (s.country) counts.set(s.country, (counts.get(s.country) ?? 0) + (s.first_party ? 2 : 1));41 let best: string | null = null;42 let n = 0;43 for (const [c, k] of counts) if (k > n) [best, n] = [c, k];44 return best;45}4647export default async function EntityPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<Search> }) {48 const { id } = await params;49 const sp = await searchParams;50 const d = await api.entity(id);51 if (!d) notFound();52 const e = d.entity;53 const tab: Tab = TABS.includes(sp.tab as Tab) ? (sp.tab as Tab) : "overview";54 const ins = d.insights ?? EMPTY_INSIGHTS;55 const silent = d.silent ?? [];56 const related = d.related ?? [];57 const country = mainCountry(d);58 const active = withinLast(e.last_event_at, 7 * 86400e3);59 const anomalous = ins.anomaly.score >= 60;60 const delta = ins.events_24h - ins.events_prev_24h;61 const href = (t: Tab): string => (t === "overview" ? `/entity/${e.id}` : `/entity/${e.id}?tab=${t}`);6263 return (64 <>65 <PageHeader66 compact67 kicker={68 <span className="flex flex-wrap items-center gap-x-2 gap-y-1">69 <TypeChip type={e.type} href={`/entities?tab=all&type=${e.type}`} />70 {d.parent && (71 <Link href={`/entity/${d.parent.id}`} className="inline-flex items-center gap-1 text-fg-muted hover:text-fg" title={`Parent: ${d.parent.name}`}>72 <span className="text-fg-subtle">↑</span> {d.parent.name}73 </Link>74 )}75 {e.domain && <Link href={`/domain/${e.domain}`} className="font-mono text-fg-muted hover:text-fg hover:underline">{e.domain}</Link>}76 {country && <Flag code={country} className="text-[12px]" />}77 <Chip tone={active ? "signal" : "default"} className="font-mono font-semibold tracking-wider" title={active ? "Events detected in the last 7 days" : "No event in the last 7 days"}>78 <span className={`inline-block size-1.5 rounded-full ${active ? "bg-signal" : "bg-low"}`} /> {active ? "ACTIVE" : "QUIET"}79 </Chip>80 {ins.rank.rank !== null && (81 <span className="font-mono text-fg-muted tabular" title={`WebSensor rank score ${fmtScore(ins.rank.score)} — importance × activity × velocity × quality × confirmation`}>82 <span className="text-fg-subtle">WebSensor rank</span> <Link href="/entities" className="font-semibold text-fg hover:underline">#{fmtInt(ins.rank.rank)}</Link> <span className="text-fg-subtle">of {fmtInt(ins.rank.total)}</span>83 </span>84 )}85 </span>86 }87 title={e.name}88 description={e.description}89 actions={90 <>91 <WatchButton kind="entity" value={e.id} />92 <Link href={`/alerts?entity=${encodeURIComponent(e.id)}`} className="rounded-md border border-line bg-panel px-2.5 py-1 text-[12px] hover:border-line-strong">Add to alert</Link>93 {(e.homepage || e.domain) && <ExtLink href={e.homepage ?? `https://${e.domain}`} className="text-[12px]">Website ↗</ExtLink>}94 </>95 }96 />9798 <div className="mb-3 grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-line bg-line sm:grid-cols-3 xl:grid-cols-6">99 <div className="bg-panel">100 <Stat101 label="Activity 24 h"102 value={fmtInt(ins.events_24h)}103 hint={104 <span className={delta > 0 ? "text-signal" : delta < 0 ? "text-fg-subtle" : ""}>105 {delta > 0 ? "↑" : delta < 0 ? "↓" : "→"} {Math.abs(delta)} vs prev 24 h · ×{ins.velocity_ratio.toFixed(2)}106 </span>107 }108 />109 </div>110 <div className="bg-panel"><Stat label="Breaking 24 h" value={fmtInt(ins.breaking_24h)} tone={ins.breaking_24h > 0 ? "hot" : undefined} hint="signal ≥ 80" /></div>111 <div className="bg-panel"><Stat label="Silent 24 h" value={fmtInt(ins.silent_24h)} tone={ins.silent_24h > 0 ? "silent" : undefined} hint="no matching announcement" /></div>112 <div className="bg-panel"><Stat label="Sources 24 h" value={fmtInt(ins.sources_24h)} hint={`${fmtInt(d.sources.length)} linked`} /></div>113 <div className="bg-panel"><Stat label="30-day baseline" value={<>{ins.baseline_per_day.toFixed(1)}<span className="text-[11px] font-normal text-fg-subtle">/day</span></>} hint={`today ${fmtInt(ins.today)}`} /></div>114 <div className="bg-panel">115 <Stat116 label={anomalous ? "Anomalous activity" : "Anomaly"}117 value={ins.anomaly.score > 0 ? fmtScore(ins.anomaly.score) : "0"}118 tone={anomalous ? "hot" : ins.anomaly.score >= 40 ? "warn" : undefined}119 hint={<span className={anomalous ? "font-semibold text-hot" : ""}>{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% vs baseline</span>}120 />121 </div>122 </div>123124 <div className="panel mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-3 py-2">125 <span className="label">35-day activity</span>126 <div className="min-w-0"><Heatmap days={ins.heatmap} /></div>127 <span className="ml-auto flex items-center gap-1 text-[10.5px] text-fg-subtle">128 less <span className="size-2.5 rounded-[2px] heat-1" /><span className="size-2.5 rounded-[2px] heat-2" /><span className="size-2.5 rounded-[2px] heat-3" /><span className="size-2.5 rounded-[2px] heat-4" /> more129 <span className="ml-2 size-2.5 rounded-[2px] heat-hot" /> ≥ 3 breaking130 </span>131 </div>132133 <Tabs134 className="mb-3"135 current={tab}136 items={[137 { key: "overview", label: "Overview", href: href("overview") },138 { key: "live", label: "Live", href: href("live") },139 { key: "sources", label: "Sources", href: href("sources"), count: d.sources.length },140 { key: "silent", label: <span className={silent.length ? "text-silent" : ""}>Silent</span>, href: href("silent"), count: silent.length },141 { key: "timeline", label: "Timeline", href: href("timeline") },142 { key: "related", label: "Related", href: href("related"), count: related.length + d.children.length },143 { key: "metrics", label: "Metrics", href: href("metrics") },144 ]}145 />146147 {tab === "overview" && <Overview d={d} ins={ins} />}148 {tab === "live" && <LiveFeed initial={d.recent} initialCursor={d.nextCursor ?? null} extraQuery={{ entity: e.id }} showTabs={false} title={`LIVE · ${e.name.toUpperCase()}`} />}149 {tab === "sources" && <SourcesTab d={d} />}150 {tab === "silent" && <SilentTab items={silent} name={e.name} />}151 {tab === "timeline" && <TimelineTab id={e.id} range={sp.range} cursor={sp.cursor} />}152 {tab === "related" && <RelatedTab d={d} />}153 {tab === "metrics" && <MetricsTab d={d} ins={ins} />}154 </>155 );156}157158// ---------------------------------------------------------------------------------------159160function Overview({ d, ins }: { d: EntityDetail; ins: EntityInsights }) {161 const e = d.entity;162 const related = d.related ?? [];163 return (164 <div className="grid gap-4 xl:grid-cols-[1fr_340px]">165 <Panel title={<>Recent signals <span className="font-mono text-fg-subtle">{d.recent.length}</span></>} dense action={<Link href={`/entity/${e.id}?tab=timeline`} className="text-[11px] text-fg-subtle hover:text-fg">full timeline →</Link>}>166 {d.recent.length ? d.recent.map((ev) => <EventRow key={ev.id} ev={ev} showDate />) : <Empty>No event detected yet for {e.name}. Sensors linked to this entity are checked continuously.</Empty>}167 </Panel>168 <aside className="flex min-w-0 flex-col gap-4">169 <SensorsPanel sensors={ins.most_active_sensors} />170 <Panel title="Related entities" dense>171 {related.length ? (172 <div className="flex flex-wrap gap-1 p-2">173 {related.slice(0, 16).map((r) => (174 <Chip key={r.id} href={`/entity/${r.id}`} title={`${typeLabel(r.type)} · ${r.shared_events} shared event${r.shared_events === 1 ? "" : "s"}`}>175 {r.name} <span className="font-mono text-fg-subtle tabular">{r.shared_events}</span>176 </Chip>177 ))}178 </div>179 ) : (180 <Empty>No co-occurring entity yet.</Empty>181 )}182 </Panel>183 {d.children.length > 0 && (184 <Panel title={<>Products & children <span className="font-mono text-fg-subtle">{d.children.length}</span></>} dense>185 <ul className="divide-y divide-line">186 {d.children.slice(0, 12).map((c) => (187 <li key={c.id} className="flex items-center justify-between gap-2 px-3 py-1.5 text-[12.5px]">188 <Link href={`/entity/${c.id}`} className="min-w-0 truncate font-medium hover:underline">{c.name}</Link>189 <span className="shrink-0 font-mono text-[11px] text-fg-subtle tabular">{typeLabel(c.type)} · {fmtInt(c.event_count)}</span>190 </li>191 ))}192 {d.children.length > 12 && <li className="px-3 py-1.5 text-[11px] text-fg-subtle"><Link href={`/entity/${e.id}?tab=related`} className="hover:text-fg">+{d.children.length - 12} more →</Link></li>}193 </ul>194 </Panel>195 )}196 {d.relations.length > 0 && (197 <Panel title="Knowledge graph" dense>198 <ul className="divide-y divide-line">199 {d.relations.slice(0, 10).map((r, i) => (200 <li key={`${r.from_id}-${r.relation}-${r.to_id}-${i}`} className="flex flex-wrap items-center gap-x-1.5 px-3 py-1.5 text-[12.5px]">201 <Link href={`/entity/${r.from_id}`} className={r.from_id === e.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.from_name}</Link>202 <span className="font-mono text-[10.5px] uppercase tracking-wider text-fg-subtle">{r.relation.replace(/_/g, " ")}</span>203 <Link href={`/entity/${r.to_id}`} className={r.to_id === e.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.to_name}</Link>204 <Chip className="ml-auto">{typeLabel(r.to_type)}</Chip>205 </li>206 ))}207 </ul>208 </Panel>209 )}210 {d.aliases.length > 0 && (211 <Panel title="Aliases">212 <div className="flex flex-wrap gap-1">{d.aliases.map((a) => <Chip key={a} className="font-mono">{a}</Chip>)}</div>213 </Panel>214 )}215 </aside>216 </div>217 );218}219220function SensorsPanel({ sensors, title = "Most active sensors · 7 d" }: { sensors: EntityInsights["most_active_sensors"]; title?: string }) {221 return (222 <Panel title={title} dense>223 {sensors.length ? (224 <ul className="divide-y divide-line">225 {sensors.slice(0, 8).map((s) => (226 <li key={s.id} className="grid grid-cols-[1fr_auto_2.5rem] items-center gap-x-2 px-3 py-1.5 text-[12.5px] hover:bg-panel-2/60">227 <div className="min-w-0">228 <Link href={`/sensor/${s.id}`} className="block truncate font-medium hover:underline">{s.name}</Link>229 <div className="flex min-w-0 items-center gap-1.5 font-mono text-[10.5px] text-fg-subtle tabular">230 <Link href={`/source/${s.source_id}`} className="truncate uppercase tracking-wide hover:text-fg">{s.source_id}</Link>231 <span className="shrink-0">· {relTime(s.last_event_at)}</span>232 </div>233 </div>234 <Chip className="font-mono">{s.type}</Chip>235 <span className="text-right font-mono text-[12px] font-semibold text-signal tabular" title="Events in the last 7 days">{s.events_7d}</span>236 </li>237 ))}238 </ul>239 ) : (240 <Empty>No sensor produced an event for this entity in the last 7 days.</Empty>241 )}242 </Panel>243 );244}245246function SourcesTab({ d }: { d: EntityDetail }) {247 return (248 <Panel title={<>Monitored sources <span className="font-mono text-fg-subtle">{d.sources.length}</span></>} dense>249 {d.sources.length === 0 ? (250 <Empty>No monitored source is linked to {d.entity.name} yet. Events still resolve to it from third-party coverage.</Empty>251 ) : (252 <Table head={["Source", "Tier", "Evidence", "Country", "Sensors", "24 h", ""]}>253 {[...d.sources].sort((a, b) => (b.events_24h ?? 0) - (a.events_24h ?? 0) || Number(Boolean(b.first_party)) - Number(Boolean(a.first_party))).map((s) => (254 <tr key={s.id} className="hover:bg-panel-2/60">255 <Td>256 <Link href={`/source/${s.id}`} className="font-medium hover:underline">{s.name}</Link>257 <div className="truncate font-mono text-[11px] text-fg-subtle">{s.domain}</div>258 </Td>259 <Td><TierBadge tier={s.tier} /></Td>260 <Td>{s.first_party ? <Badge kind="first-party" /> : <Badge kind="external" />}</Td>261 <Td>{s.country ? <span className="inline-flex items-center gap-1.5"><Flag code={s.country} /><span className="font-mono text-[11px] text-fg-subtle">{s.country}</span></span> : <span className="text-fg-subtle">—</span>}</Td>262 <Td mono>{s.sensor_count ?? 0}</Td>263 <Td mono className={s.events_24h ? "text-signal" : "text-fg-subtle"}>{s.events_24h ?? 0}</Td>264 <Td className="text-right"><Link href={`/source/${s.id}`} className="whitespace-nowrap text-[11.5px] text-fg-subtle hover:text-fg">sensors →</Link></Td>265 </tr>266 ))}267 </Table>268 )}269 </Panel>270 );271}272273function SilentTab({ items, name }: { items: EventItem[]; name: string }) {274 return (275 <Panel title={<span className="inline-flex items-center gap-2"><Badge kind="silent" /> Silent changes <span className="font-mono text-fg-subtle">{items.length}</span></span>} dense action={<Link href={`/live?entity=${encodeURIComponent(name)}&silent_change=true`} className="hidden text-[11px] text-fg-subtle hover:text-fg sm:inline">all silent →</Link>}>276 {items.length === 0 ? (277 <Empty>No silent changes detected in this period.</Empty>278 ) : (279 <ul className="divide-y divide-line">280 {items.map((ev) => (281 <li key={ev.id} className="grid grid-cols-[auto_1fr] gap-x-3 border-l-2 border-l-silent/60 px-3 py-2 sm:grid-cols-[6.25rem_1fr_auto]">282 <div className="flex flex-col font-mono text-[11px] leading-4 text-fg-subtle tabular">283 <time dateTime={ev.detected_at} title={utcDateTime(ev.detected_at)} className="text-fg-muted">{utcTime(ev.detected_at)}</time>284 <span>{utcDate(ev.detected_at)}</span>285 </div>286 <div className="min-w-0 sm:col-start-2">287 <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px]">288 <Link href={`/source/${ev.source?.id ?? ev.source_id}`} className="font-mono font-semibold uppercase tracking-wide text-fg-muted hover:text-fg">{ev.source?.name ?? ev.source_id}</Link>289 {ev.country && <Flag code={ev.country} />}290 <span className="sm:hidden"><Score value={ev.signal_score ?? ev.importance} size="sm" kind="signal" /></span>291 <Chip>{typeLabel(ev.event_type)}</Chip>292 {ev.change_class && <Chip tone="silent">{ev.change_class}</Chip>}293 </div>294 <Link href={`/event/${ev.slug}`} className="mt-0.5 block font-medium leading-snug hover:underline">{ev.title}</Link>295 {ev.field_changes && ev.field_changes.length > 0 ? (296 <div className="mt-1.5 max-w-3xl"><FieldChanges items={ev.field_changes} compact max={4} /></div>297 ) : (298 ev.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-fg-muted">{ev.summary}</p>299 )}300 </div>301 <div className="hidden items-start pt-0.5 sm:flex"><Score value={ev.signal_score ?? ev.importance} kind="signal" /></div>302 </li>303 ))}304 </ul>305 )}306 </Panel>307 );308}309310async function TimelineTab({ id, range, cursor }: { id: string; range?: string; cursor?: string }) {311 const r = RANGES.find((x) => x.key === range) ?? RANGES[2]!;312 const after = r.ms ? agoIso(r.ms) : undefined;313 const page = await api.events({ entity: id, after, limit: 100, cursor });314 const groups = new Map<string, EventItem[]>();315 for (const ev of page.items) {316 const day = utcDate(ev.detected_at);317 if (!groups.has(day)) groups.set(day, []);318 groups.get(day)!.push(ev);319 }320 const rangeHref = (k: string): string => `/entity/${id}?tab=timeline&range=${k}`;321 return (322 <Panel323 title={<>Timeline <span className="font-mono text-fg-subtle">{page.items.length}{page.nextCursor ? "+" : ""}</span></>}324 dense325 action={326 <nav className="flex items-center gap-0.5" aria-label="Range">327 {RANGES.map((x) => (328 <Link key={x.key} href={rangeHref(x.key)} aria-current={x.key === r.key ? "page" : undefined} className={`rounded-sm px-1.5 py-px font-mono text-[11px] tabular ${x.key === r.key ? "bg-panel-2 text-fg" : "text-fg-subtle hover:text-fg"}`}>{x.label}</Link>329 ))}330 </nav>331 }332 >333 {page.items.length === 0 ? (334 <Empty>No event in the last {r.label.replace(" ", "")}{r.ms ? "" : " — nothing recorded yet"}. Try a wider range.</Empty>335 ) : (336 <div>337 {[...groups.entries()].map(([day, evs]) => (338 <div key={day}>339 <div className="sticky top-12 z-10 flex items-center justify-between border-y border-line bg-panel-2/95 px-3 py-1 font-mono text-[11px] font-semibold tracking-wider text-fg-muted backdrop-blur">340 <span>{dayHeader(evs[0]!.detected_at)}</span>341 <span className="font-normal text-fg-subtle tabular">{evs.length}</span>342 </div>343 {evs.map((ev) => <EventRow key={ev.id} ev={ev} />)}344 </div>345 ))}346 </div>347 )}348 <div className="flex items-center justify-between px-3 py-2 text-[12px] text-fg-subtle">349 <span>{after ? <>since <span className="font-mono tabular">{utcDateTime(after)}</span></> : "complete history, newest first"}</span>350 {page.nextCursor && <Link href={`${rangeHref(r.key)}&cursor=${encodeURIComponent(page.nextCursor)}`} className="rounded-md border border-line bg-panel-2 px-2.5 py-1 text-fg hover:border-line-strong">Older →</Link>}351 </div>352 </Panel>353 );354}355356function RelatedTab({ d }: { d: EntityDetail }) {357 const related = d.related ?? [];358 const max = Math.max(1, ...related.map((r) => r.shared_events));359 return (360 <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">361 <Panel title={<>Co-occurring entities <span className="font-mono text-fg-subtle">{related.length}</span></>} dense>362 {related.length === 0 ? (363 <Empty>No entity shares an event with {d.entity.name} yet.</Empty>364 ) : (365 <Table head={["Entity", "Type", "Shared events", ""]}>366 {related.map((r) => (367 <tr key={r.id} className="hover:bg-panel-2/60">368 <Td><Link href={`/entity/${r.id}`} className="font-medium hover:underline">{r.name}</Link></Td>369 <Td><Chip>{typeLabel(r.type)}</Chip></Td>370 <Td mono>{r.shared_events}</Td>371 <Td className="w-32 min-w-24 pt-3"><Bar value={r.shared_events} max={max} tone="info" /></Td>372 </tr>373 ))}374 </Table>375 )}376 </Panel>377 <div className="flex flex-col gap-4">378 <Panel title={<>Products & children <span className="font-mono text-fg-subtle">{d.children.length}</span></>} dense>379 {d.children.length === 0 ? (380 <Empty>No child entity.</Empty>381 ) : (382 <Table head={["Entity", "Type", "Events", "Last"]}>383 {d.children.map((c) => (384 <tr key={c.id} className="hover:bg-panel-2/60">385 <Td><Link href={`/entity/${c.id}`} className="font-medium hover:underline">{c.name}</Link></Td>386 <Td><Chip>{typeLabel(c.type)}</Chip></Td>387 <Td mono>{fmtInt(c.event_count)}</Td>388 <Td mono className="whitespace-nowrap text-fg-subtle">{c.last_event_at ? relTime(c.last_event_at) : "—"}</Td>389 </tr>390 ))}391 </Table>392 )}393 </Panel>394 <Panel title={<>Knowledge graph <span className="font-mono text-fg-subtle">{d.relations.length}</span></>} dense>395 {d.relations.length === 0 ? (396 <Empty>No relation recorded.</Empty>397 ) : (398 <ul className="divide-y divide-line">399 {d.relations.map((r, i) => (400 <li key={`${r.from_id}-${r.relation}-${r.to_id}-${i}`} className="flex flex-wrap items-center gap-x-1.5 px-3 py-1.5 text-[12.5px]">401 <Link href={`/entity/${r.from_id}`} className={r.from_id === d.entity.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.from_name}</Link>402 <span className="font-mono text-[10.5px] uppercase tracking-wider text-fg-subtle">{r.relation.replace(/_/g, " ")}</span>403 <Link href={`/entity/${r.to_id}`} className={r.to_id === d.entity.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.to_name}</Link>404 <Chip className="ml-auto">{typeLabel(r.to_type)}</Chip>405 </li>406 ))}407 </ul>408 )}409 </Panel>410 {d.aliases.length > 0 && (411 <Panel title="Aliases">412 <div className="flex flex-wrap gap-1">{d.aliases.map((a) => <Chip key={a} className="font-mono">{a}</Chip>)}</div>413 </Panel>414 )}415 </div>416 </div>417 );418}419420function MetricsTab({ d, ins }: { d: EntityDetail; ins: EntityInsights }) {421 const maxType = Math.max(1, ...d.by_type.map((t) => t.n));422 const total = d.by_type.reduce((n, t) => n + t.n, 0);423 const anomalous = ins.anomaly.score >= 60;424 const days7 = ins.heatmap.slice(-7).reduce((n, x) => n + x.events, 0);425 const days35 = ins.heatmap.reduce((n, x) => n + x.events, 0);426 const peak = ins.heatmap.reduce<{ day: string; events: number } | null>((best, x) => (!best || x.events > best.events ? { day: x.day, events: x.events } : best), null);427 return (428 <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">429 <div className="flex flex-col gap-4">430 <Panel title={<>Event types <span className="font-mono text-fg-subtle">{fmtInt(total)}</span></>}>431 {d.by_type.length ? (432 <ul className="space-y-1.5">433 {d.by_type.map((t) => (434 <li key={t.event_type} className="grid grid-cols-[9rem_1fr_3.5rem] items-center gap-2 text-[12px] sm:grid-cols-[11rem_1fr_3.5rem]">435 <Link href={`/live?entity=${encodeURIComponent(d.entity.id)}&event_type=${t.event_type}`} className="truncate hover:underline">{typeLabel(t.event_type)}</Link>436 <Bar value={t.n} max={maxType} tone="info" />437 <span className="text-right font-mono text-fg-subtle tabular">{t.n} <span className="text-[10.5px]">{total ? Math.round((t.n / total) * 100) : 0}%</span></span>438 </li>439 ))}440 </ul>441 ) : (442 <Empty>No events yet.</Empty>443 )}444 </Panel>445 <SensorsPanel sensors={ins.most_active_sensors} />446 </div>447 <div className="flex flex-col gap-4">448 <Panel title="Anomaly">449 <div className="flex items-baseline justify-between">450 <span className={`font-mono text-3xl font-semibold tabular ${anomalous ? "text-hot" : ins.anomaly.score >= 40 ? "text-warn" : "text-fg"}`}>{ins.anomaly.score > 0 ? fmtScore(ins.anomaly.score) : "0"}</span>451 <span className={`label ${anomalous ? "!text-hot" : ""}`}>{anomalous ? "ANOMALOUS ACTIVITY" : ins.anomaly.score >= 40 ? "ELEVATED" : "NORMAL"}</span>452 </div>453 <div className="mt-2"><Bar value={ins.anomaly.score} tone={anomalous ? "hot" : ins.anomaly.score >= 40 ? "high" : "signal"} /></div>454 <dl className="mt-3 grid grid-cols-2 gap-y-1 text-[12.5px]">455 <dt className="text-fg-subtle">30-day baseline</dt><dd className="text-right font-mono tabular">{ins.baseline_per_day.toFixed(1)} events/day</dd>456 <dt className="text-fg-subtle">Last 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(ins.events_24h)} events</dd>457 <dt className="text-fg-subtle">Previous 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(ins.events_prev_24h)} events</dd>458 <dt className="text-fg-subtle">Velocity ratio</dt><dd className="text-right font-mono tabular">×{ins.velocity_ratio.toFixed(2)}</dd>459 <dt className="text-fg-subtle">vs baseline</dt><dd className={`text-right font-mono tabular ${anomalous ? "font-semibold text-hot" : ""}`}>{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% (×{ins.anomaly.ratio.toFixed(2)})</dd>460 </dl>461 <p className="mt-3 text-[12px] leading-relaxed text-fg-muted">462 The anomaly score compares the last 24 h with this entity's own 30-day baseline, so a busy organization is not flagged for being busy. Scores ≥ 60 mark anomalous activity; the ratio above 1 means more events than usual.463 </p>464 </Panel>465 <Panel title="35-day activity">466 <Heatmap days={ins.heatmap} />467 <dl className="mt-3 grid grid-cols-3 gap-2 text-[12.5px]">468 <div><dt className="label">7 d</dt><dd className="font-mono text-base font-semibold tabular">{fmtInt(days7)}</dd></div>469 <div><dt className="label">35 d</dt><dd className="font-mono text-base font-semibold tabular">{fmtInt(days35)}</dd></div>470 <div className="min-w-0"><dt className="label">Peak day</dt><dd className="truncate font-mono text-base font-semibold tabular">{peak && peak.events ? <>{peak.events} <span className="text-[11px] font-normal text-fg-subtle">{peak.day}</span></> : "—"}</dd></div>471 </dl>472 </Panel>473 <div className="grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-line bg-line">474 <div className="bg-panel"><Stat label="Total events" value={fmtInt(d.entity.event_count)} hint={d.entity.last_event_at ? `last ${relTime(d.entity.last_event_at)}` : "none yet"} /></div>475 <div className="bg-panel"><Stat label="Entity importance" value={fmtScore(d.entity.importance)} hint="weight in scoring" /></div>476 </div>477 </div>478 </div>479 );480}481